package a2;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Insets;
import java.awt.LayoutManager;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.util.Iterator;
import java.util.LinkedList;


public class A2Layout implements LayoutManager{
	
	int prefheight, prefwidth, 
		minimumheight, minimumwidth,
		blocksTall, blocksWide,
		blockHeight, blockWidth,
		maxComponents; 
	//this boolean will be used to keep track of whether the screen is in portrait or landscape
	Boolean portrait, preferredKnown, minimumKnown;
	double aspectr;
	LinkedList<ConstrainedComponent> components = new LinkedList<ConstrainedComponent>();
	int [][] grid;
	Component parent;
	ComponentListener listener = new ComponentListener() {
	      public void componentResized(ComponentEvent e) {
	    	  layoutContainer((Container) e.getComponent());
	      }
	      public void componentHidden(ComponentEvent e) {
	      }
	      public void componentMoved(ComponentEvent e) {
	      }
	      public void componentShown(ComponentEvent e) {
	      }
	};
	
	
	/**
	 *Initializes a layout of size gridX * gridY, belonging to parent.
	 *It maintains the specified aspect ratio.
	 *@param parent The parent container
	 *@param gridX The number of columns in the grid.
	 *@param grid YThe number of rows in the grid.
	 *@param ratio The desired aspect ratio, given as a double.
	 *@throws ArithmeticException if gridX,gridY, or ratio are not greater than zero.
	 *@throws NullPointerException if parent is null.
	 */
	public A2Layout(Container parent, int gridX, int gridY, double ratio)
	{
		if(gridX <= 0 || gridY <= 0 || ratio <= 0)
			throw new ArithmeticException("Value out of range: Values must be greater than zero.");
		if(parent == null)
			throw new NullPointerException("Parent container may not be empty.");
		this.aspectr = ratio;
		if(this.aspectr <= 0.0)
			this.aspectr = 16.0/9.0;
		this.preferredKnown = false;
		this.minimumKnown = false;
		this.portrait = ratio < 1;
		this.blocksWide = gridX;
		this.blocksTall = gridY;
		this.grid = new int[gridX +1][gridY+1];
		for(int i = 0; i < gridX+1; i++)
			for(int k = 0; k < gridY+1; k++)
				this.grid [i][k] = 0;
		this.maxComponents = gridX*gridY;
		parent.addComponentListener(listener);
		this.parent = parent;
	}
	

	@Override
	/**
	 *Calculates the minimum size of our window based on the size of
	 *provided components.
	 *@param parent The parent container.
	 *@return The Dimension which describes the minimum layout size.
	 */
	public Dimension minimumLayoutSize(Container parent) {
		Dimension d = new Dimension();
		Insets par = parent.getInsets();
		int tallest = 0;
		int widest = 0;
		double y,x;
		Iterator<ConstrainedComponent> it = this.components.iterator();
		ConstrainedComponent c;
		while(it.hasNext())
		{
			c = it.next();
			try{
				if (c.component().getPreferredSize().width / c.constraints().width() > widest)
				{	
					if(c.component().getPreferredSize().width / c.constraints().width() >= c.component().getPreferredSize().width)
						widest = c.component().getPreferredSize().width / c.constraints().width();
					else
						widest = c.component().getPreferredSize().width;
				}
			}
			catch(ArithmeticException e)
			{
				if (c.component().getPreferredSize().width / this.blocksWide > widest)
				{	
					if(c.component().getPreferredSize().width / this.blocksWide >= c.component().getPreferredSize().width)
						widest = c.component().getPreferredSize().width / this.blocksWide;
					else
						widest = c.component().getPreferredSize().width;
				}
			}
			try{
				if(c.component().getPreferredSize().height / c.constraints().height() > tallest)
				{
					if(c.component().getPreferredSize().height / c.constraints().height() >= c.component().getPreferredSize().height)
					tallest = c.component().getPreferredSize().height / c.constraints().height();
					else
						tallest = c.component().getPreferredSize().height;
				}
			}
			catch(ArithmeticException e)
			{
				if(c.component().getPreferredSize().height / this.blocksTall > tallest)
				{
					if(c.component().getPreferredSize().height / this.blocksTall >= c.component().getPreferredSize().height)
					tallest = c.component().getPreferredSize().height / this.blocksTall;
					else
						tallest = c.component().getPreferredSize().height;
				}
			}
		}
		y = tallest * this.blocksTall + par.top + par.bottom;
		x = widest * this.blocksWide + par.left + par.right;
		if((x / y < 1))
		{
			x = y * this.aspectr;
		}
		else
			y = x / this.aspectr;
		this.minimumheight = (int) y;
		this.minimumwidth = (int) x;
		d.width = this.minimumwidth;
		d.height = this.minimumheight;
		if(this.minimumheight >  0 && this.minimumwidth > 0)
			this.minimumKnown = true;
		this.blockHeight = this.minimumheight / this.blocksTall;
		this.blockWidth = this.minimumwidth / this.blocksWide;
		return d;
	}
	

	@Override
	/**
	 * Finds the size of the layout based on the parent's size.
	 * @param parent The parent container.
	 * @return The optimal size of the layout in the given space.
	 */
	public Dimension preferredLayoutSize(Container parent) {
		Dimension d = minimumLayoutSize(parent);
		double x = parent.getWidth();
		double y = parent.getHeight();
		if((x / y < 1))
		{
			x = y * this.aspectr;
		}
		else
			y = x / this.aspectr;
		this.prefheight = (int) y;
		this.prefwidth = (int) x;
		Dimension minimum = this.minimumLayoutSize(parent);
		if(portrait)
			if(minimum.height > this.prefheight)
			{
				this.prefheight = minimum.height;
				this.prefwidth = minimum.width;
				d = minimum;
			}
		if(!portrait)
			if(minimum.width > this.prefwidth)
			{
				this.prefwidth = minimum.width;
				this.prefheight = minimum.height;
				d = minimum;
			}
		d.width = this.prefwidth;
		d.height = this.prefheight;
		if(this.prefheight >  0 && this.prefwidth > 0)
			this.preferredKnown = true;
		this.blockHeight = this.prefheight / this.blocksTall;
		this.blockWidth = this.prefwidth / this.blocksWide;
		
		return d;
	}
	
	/**
	 * Provides the size of a grid cell.  If the layout does not currently contain
	 * Any components, a dimension of 0,0 will be returned.  The first component added
	 * will have its dimensions used as the grid size.
	 * @return The current dimensions of a grid cell.
	 */
	public Dimension gridSize()
	{
		return new Dimension(this.blockWidth, this.blockHeight);
	}

	/**
	 * Adds a new component to the layout, returning a Boolean describing whether it was 
	 * successful.  If this is the first component to be added, the grid will be sized
	 * according to the component's dimensions.
	 * @param arg0 The component to be added.
	 * @param constraints The constraints which the component will conform to.
	 * @return True if the addition was successful, false otherwise.
	 * @throws ArithmeticException If components are placed outside of the grid.
	 */
	public Boolean addLayoutComponent(Component arg0, A2Constraints constraints) {
		/*
		 * new elements should be added as follows
		 * layoutobject.addLayoutComponent(component,constraints);
         * jframe.add(component);
         * then to make it visible, lay the countainer out again:
         * layoutobject.layoutContainer(jframe);
		 */

		constraints.setXPos(constraints.xPos());
		constraints.setYPos(constraints.yPos());
		if(((constraints.xPos() < 1) || constraints.xPos() > this.blocksWide) 
				|| ((constraints.yPos() < 1) || (constraints.yPos() > this.blocksTall)))
			throw new ArithmeticException("Components placed outside of grid area");
		if(this.fitsInHole(new ConstrainedComponent(constraints,arg0)))
		{
			ConstrainedComponent component = new ConstrainedComponent(constraints,arg0);
			this.components.add(component);
			//use the just-added component to claim the grid space.
			//linkedlist.add places the new item at the end of the list.
			this.claimGrid(component);
			this.preferredLayoutSize((Container) this.parent);
			return true;
		}
		else 
			return false;
	}


	@Override
	/**
	 * Removes a given component from the layout.
	 * @param comp the component to be removed.
	 */
	public void removeLayoutComponent(Component comp) {
		if(!components.isEmpty())
		{
			Iterator<ConstrainedComponent> it = this.components.iterator();
			Boolean found = false;
			ConstrainedComponent c;
			while(it.hasNext() && !found)
			{
				c = it.next();
				if(c.component().equals(comp))
				{
					found = true;
					this.cleanGrid(c);
					this.components.remove(c);
				}
			}
		}
		this.layoutContainer((Container)this.parent);
	}
	
	
	

	@Override
	/**
	 *Lays out all components currently assigned to the layout manager.
	 *@param parent The parent container of the layout.
	 */
	public void layoutContainer(Container parent) {
		this.preferredLayoutSize(parent);
		//determine how large the grid positions are.
		this.blockHeight = this.prefheight / this.blocksTall;
		this.blockWidth = this.prefwidth / this.blocksWide;
		Iterator<ConstrainedComponent> it = this.components.iterator();
		ConstrainedComponent c;
		Dimension use;
		while(it.hasNext())
		{
			c = it.next();
			A2Constraints cons = c.constraints();
			//if c fits without modification, add it
			use = squish(c);
			if (use != null)
			{
				use = c.component().getPreferredSize();
				//the bounds provided are found by adding the parent's insets,
				//then adding the desired grid position times the size of
				//a grid position.  The positions have -1 modifiers because the grid begins at 1,1,
				// rather than 0,0, and the size of the blocks are not to be used in their own
				//position calculations.
				int y = parent.getInsets().top +((this.blockHeight) * (cons.yPos() -1));
				int x = parent.getInsets().left +((this.blockWidth) * (cons.xPos() -1));
				c.component().setLocation(x, y);
				c.component().setSize(use);
			}
		}
	}
	
	/**
	 * Determines whether a given component will fit in its desired space.
	 * @param comp The component to be tested.
	 * @return True if the component can fit, false otherwise.
	 */
	private Boolean fitsInHole(ConstrainedComponent comp)
	{
		Boolean fits = true;

		if(this.squish(comp) == null)
			fits = false;
		return (fits && this.freeGrid(comp));
	}
	
	/**
	 * Determines whether a component can be made to fit in its desired
	 * area, and returns the required dimensions.  A component cannot be
	 * reduced by more than half its size.
	 * @param comp The component to be checked.
	 * @return Returns the required dimensions of the component if it can be fit, null otherwise.
	 */
	private Dimension squish(ConstrainedComponent comp)
	{
		Dimension d = comp.component().getPreferredSize();
		int requestedHeight = d.height;
		int requestedWidth = d.width;
		int requestedAreaY = this.blockHeight * comp.constraints().height();
		int requestedAreaX = this.blockWidth * comp.constraints().width();
		if(requestedHeight / comp.constraints().height() > this.blockHeight)
		{
			int difference = requestedHeight - requestedAreaY;
			if((requestedHeight - difference) > requestedHeight / 2)
				requestedHeight = requestedHeight - difference;
		}
		if(requestedWidth > requestedAreaX)
		{
			int difference = requestedWidth - requestedAreaX;
			if((requestedWidth - difference) > requestedWidth / 2)
				requestedWidth = requestedWidth - difference;
		}
		if(requestedAreaY == 0)
			requestedHeight = this.prefheight;
		if(requestedAreaX == 0)
			requestedWidth = this.prefwidth;
		d.setSize(requestedWidth, requestedHeight);

		return d;
	}
	
	
	/**
	 * Determines whether the grid positions requested by comp are available.
	 * @param comp The given component.
	 * @return True if the grid positions are available, otherwise false.
	 */
	private Boolean freeGrid(ConstrainedComponent comp)
	{
		Boolean free = true;
		int xMin = comp.constraints().xPos();
		int xMax = comp.constraints().width() + xMin;
		int yMin = comp.constraints().yPos();
		int yMax = comp.constraints().height() + yMin;
		if(comp.constraints().width() == 0)
		{
			xMin = 0;
			xMax = this.blocksWide;
		}
		if(comp.constraints().height() == 0)
		{
			yMin = 0;
			yMax = this.blocksTall;
		}
		if(yMax < this.blocksTall && xMax < this.blocksWide)
		 for(int i = xMin; i < xMax; i ++)
			for(int k = yMin; k < yMax; k ++)
			{
				if(this.grid[i][k] == 1)
					free = false;
			}
		/*
		 * check whether the grid spaces in this range are free
		 */
		return free;
	}
	
	/**
	 * Reserves grid positions requested by comp.
	 * @param comp The given component.
	 */
	private void claimGrid(ConstrainedComponent comp)
	{
		
		int xMin = comp.constraints().xPos() ;
		int xMax = comp.constraints().width() + xMin;
		int yMin = comp.constraints().yPos();
		int yMax = comp.constraints().height() + yMin;
		if(comp.constraints().width() == 0)
		{
			xMin = 0;
			xMax = this.blocksWide;
		}
		if(comp.constraints().height() == 0)
		{
			yMin = 0;
			yMax = this.blocksTall;
		}
		for(int i = xMin; i < xMax; i++)
		{
			for(int k = yMin; k < yMax; k++)
			{
				this.grid[i][k] = 1;
			}
		}
	}
	
	/**
	 * Unclaims grid positions owned by comp.
	 * @param comp The component to be removed from the grid.
	 */
	private void cleanGrid(ConstrainedComponent comp)
	{
		int xMin = comp.constraints().xPos();
		int xMax = comp.constraints().width() + xMin;
		int yMin = comp.constraints().yPos();
		int yMax = comp.constraints().height() + yMin;
		if(comp.constraints().width() == 0)
		{
			xMin = 0;
			xMax = this.blocksWide;
		}
		if(comp.constraints().height() == 0)
		{
			yMin = 0;
			yMax = this.blocksTall;
		}
		for(int i = xMin; i < xMax; i ++)
			for(int k = yMin; k < yMax; k ++)
				this.grid[i][k] = 0;
	}
	
	/**
	 * Provides the client with access to information concerning which
	 * grid positions are available and which are claimed.  A value of
	 * 1 denotes the space is claimed, 0 denotes availability.
	 * @return The array detailing available grid positions.
	 */
	public int[][] gridMap()
	{
		return this.grid;
	}
	
	/**
	 * Facilitates a rotation of the layout.
	 * @param parent The parent container.
	 */
	public void flip(Container parent)
	{
		this.aspectr = 1/this.aspectr;
		this.portrait = !this.portrait;
		this.minimumKnown = false;
		this.preferredKnown = false;
		/*int temp = this.blocksTall;
		this.blocksTall = this.blocksWide;
		this.blocksWide = temp;
		*/
		this.preferredLayoutSize(parent);
		this.layoutContainer(parent);
	}
	
	/**
	   * Not usable by this layout manager.
	   * @param name the name of the component
	   * @param comp the component to be added
	   */
	@Override
	public void addLayoutComponent(String name, Component comp) {
		
	}
}

